package org.oy.sealogger.device;

import gnu.io.CommPortIdentifier;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;

import org.apache.log4j.Logger;
import org.oy.sealogger.common.Config;
import org.oy.sealogger.common.Messages;
import org.oy.sealogger.ds.InputAnalyser;
import org.oy.sealogger.ds.InputMetaData;

/**
 * Superclass for serial port devices. This class makes use of
 * RXTX implementation of the Java Communications API (javax.comm)
 * @author riku
 *
 */
public abstract class SerialDevice implements Device {

	public static enum PortSpeed {
		B2400(2400),
		B4800(4800),
		B9600(9600);
		
		private int value;
		
		PortSpeed(int value) { this.value = value; }
	}
	
	protected InputAnalyser inputAnalyser;
	
	private Logger log = Config.getLogger();
	private InputStream in = null;
	private SerialPort port;
	private int[] portParams = null;
	private boolean connected = false;
	
	@SuppressWarnings("unchecked")
	public final void connect() throws DeviceException {
		
		Enumeration<CommPortIdentifier> ports = CommPortIdentifier.getPortIdentifiers();
		boolean portFound = false;
		
		while(ports.hasMoreElements() && !portFound) {
			
			CommPortIdentifier cpi = ports.nextElement();
			
			if(cpi.getPortType() != CommPortIdentifier.PORT_SERIAL) continue;
			
			try {
				
				port = (SerialPort)cpi.open("SeaLogger", 2000);

				if(portParams != null)
					port.setSerialPortParams(portParams[0], portParams[1], portParams[2], portParams[3]);
				
			} catch (PortInUseException e) {
				disconnect();
				log.error(e);
				throw new DeviceException(e.getMessage());
			} catch(UnsupportedCommOperationException e) {
				disconnect();
				log.error(e);
				throw new DeviceException(e.getMessage());
			}
			
			try {
				in = port.getInputStream();
				Thread.sleep(500);
			} catch (IOException e) {
				disconnect();
				log.error(e);
				throw new DeviceException(e.getMessage());
			} catch(InterruptedException e) {
				disconnect();
				log.error(e);
				throw new DeviceException(e.getMessage());
			}

			InputMetaData imd = inputAnalyser.analyse(in);
			
			if(!imd.isOfWantedType() && ports.hasMoreElements()) {
				disconnect();
				continue;
			} else if(!imd.isOfWantedType() && !ports.hasMoreElements()) {
				disconnect();
			} else if(imd.isOfWantedType()) {
				portFound = true;
				log.debug("Using " + cpi.getName());
			}			
		}
		
		if(!portFound) {
			throw new DeviceException(Messages.getMessage("error.no_device_detected"));
		} else {
			try {
				in = port.getInputStream();
			} catch(IOException e) {
				log.error(e);
				disconnect();
				throw new DeviceException(e.getMessage());
			}
			connected = true;
			log.debug("Serial device connected");
		}
	}

	public final InputStream getInputStream() {
		return in;
	}

	public final void disconnect() throws DeviceException {
		try {
			port.close();
			in.close();
		} catch(IOException e) {
			log.error(e);
			throw new DeviceException(e.getMessage());
		} finally {
			connected = false;
			//log.debug("Serial device disconnected");
		}
	}

	public final boolean isConnected() {
		return connected;
	}
	
	@SuppressWarnings("finally")
	public final boolean dataAvailable() {
		boolean dataAvailable = false;
		
		try {
			dataAvailable = in.available() > 0;
		} catch(IOException e) {
			log.error(e);
		} finally {
			return dataAvailable;
		}
	}

	/**
	 * Set the port speed. 
	 * @param speed 4800 or 9600, other values are not allowed. 4800 or less will result in
	 * port speed of 4800, greater values in 9600.
	 * @return the port speed that was actually set
	 */
	public int setPortSpeed(PortSpeed speed) {
		portParams = new int[4];
		portParams[0] = speed.value;
		portParams[1] = SerialPort.DATABITS_8;
		portParams[2] = SerialPort.STOPBITS_1;
		portParams[3] = SerialPort.PARITY_NONE;
		
		return portParams[0];
	}

	/**
	 * Get the port speed. <b>This can only be used when the port speed has been manually set using
	 * setPortSpeed() method</b>.
	 * @return <b>The previously set</b> port speed or 0 if setPortSpeed() method has not been priorily called
	 */
	protected int getPortSpeed() {
		return portParams != null ? portParams[0] : 0;
	}
	
	@SuppressWarnings("finally")
	public final boolean test() {
		
		boolean good = false;
		
		try {
			connect();
			good = true;			
			disconnect();
			
		} catch(DeviceException e) {
			log.error(e);
		} finally {
			return good;
		}
	}

	public final void setInputAnalyser(InputAnalyser analyser) {
		this.inputAnalyser = analyser;
	}
	
	@Override
	public void finalize() {
		try {
			disconnect();
		} catch (DeviceException e) {
			e.printStackTrace();
		}
	}	
}
